﻿using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Diagnostics;

namespace DiscreteEventModel.Common {

    public class ObjectFile<TToken> {

        class BinaryWriter : IBinaryWriter {

            ObjectFile<TToken> _parent;
            private MemoryMappedViewAccessor _pageAccessor;
            private long _pageOffset;

            public BinaryWriter(ObjectFile<TToken> parent, out long entryPage) {
                this._parent = parent;
                this._pageAccessor = this.AllocatePage(out entryPage);
                this._pageOffset = 0;
            }

            public BinaryWriter(ObjectFile<TToken> parent, long entryPage) {
                this._parent = parent;
                this._pageAccessor = this._parent._mappedFile.CreateViewAccessor(entryPage, this._parent.PageSize);
                this._pageOffset = 0;
            }

            ~BinaryWriter() {
                this.Dispose();
            }

            private MemoryMappedViewAccessor AllocatePage(out long page) {
                MemoryMappedViewAccessor pageAccessor;
                if(this._parent._freePages.TryDequeue(out page)) {
                    pageAccessor = this._parent._mappedFile.CreateViewAccessor(page, this._parent.PageSize);
                    long nextPage = pageAccessor.ReadInt64(this._parent.PageCapacity);
                    if(nextPage >= 0) {
                        this._parent._freePages.Enqueue(nextPage);
                    }
                } else if(this._parent.PageCount < this._parent.MaxPageCount) {
                    page = this._parent.PageCount * this._parent.PageSize;
                    pageAccessor = this._parent._mappedFile.CreateViewAccessor(page, this._parent.PageSize);
                    this._parent.PageCount++;
                } else {
                    throw new Exception();
                }
                pageAccessor.Write(this._parent.PageCapacity, -1L);
                return pageAccessor;
            }

            private void WriteBytes(byte[] byteArray, int count) {
                for(int offset = 0; count > 0; count--) {
                    if(this._pageOffset == this._parent.PageCapacity) {
                        long nextPage = this._pageAccessor.ReadInt64(this._parent.PageCapacity);
                        if(nextPage >= 0) {
                            this._pageAccessor.Dispose();
                            this._pageAccessor = this._parent._mappedFile.CreateViewAccessor(nextPage, this._parent.PageSize);
                        } else {
                            MemoryMappedViewAccessor nextPageAccessor = this.AllocatePage(out nextPage);
                            this._pageAccessor.Write(this._parent.PageCapacity, nextPage);
                            this._pageAccessor.Dispose();
                            this._pageAccessor = nextPageAccessor;
                        }
                        this._pageOffset = 0;
                    }
                    this._pageAccessor.Write(this._pageOffset++, byteArray[offset++]);
                }
            }

            public void Dispose() {
                if(this._pageAccessor != null) {
                    long nextPage = this._pageAccessor.ReadInt64(this._parent.PageCapacity);
                    if(nextPage >= 0) {
                        this._parent._freePages.Enqueue(nextPage);
                    }
                    this._pageAccessor.Dispose();
                    this._pageAccessor = null;
                    this._parent = null;
                    this._pageOffset = -1;
                }
            }

            #region IBinaryWriter

            public void Write(UInt16 value) {
                if(this._pageOffset <= this._parent.CutOffSizeForUInt16) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(UInt16);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(UInt16));
                }
            }

            public void Write(Int16 value) {
                if(this._pageOffset <= this._parent.CutOffSizeForInt16) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Int16);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Int16));
                }
            }

            public void Write(UInt32 value) {
                if(this._pageOffset <= this._parent.CutOffSizeForUInt32) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(UInt32);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(UInt32));
                }
            }

            public void Write(Int32 value) {
                if(this._pageOffset <= this._parent.CutOffSizeForInt32) {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Int32));
                } else {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Int32);
                }
            }

            public void Write(UInt64 value) {
                if(this._pageOffset <= this._parent.CutOffSizeForUInt64) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(UInt64);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(UInt64));
                }
            }

            public void Write(Int64 value) {
                if(this._pageOffset <= this._parent.CutOffSizeForInt64) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Int64);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Int64));
                }
            }

            public void Write(Single value) {
                if(this._pageOffset <= this._parent.CutOffSizeForSingle) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Single);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Single));
                }
            }

            public void Write(Double value) {
                if(this._pageOffset <= this._parent.CutOffSizeForDouble) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Double);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Double));
                }
            }

            public void Write(Char value) {
                if(this._pageOffset <= this._parent.CutOffSizeForChar) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Char);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Char));
                }
            }

            public void Write(Boolean value) {
                if(this._pageOffset <= this._parent.CutOffSizeForBoolean) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Boolean);
                } else {
                    this.WriteBytes(BitConverter.GetBytes(value), sizeof(Boolean));
                }
            }

            public void Write(Byte value) {
                if(this._pageOffset <= this._parent.CutOffSizeForByte) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Byte);
                } else {
                    this.WriteBytes(new byte[] { value }, sizeof(Byte));
                }
            }

            public void Write(Decimal value) {
                if(this._pageOffset <= this._parent.CutOffSizeForDecimal) {
                    this._pageAccessor.Write(this._pageOffset, value);
                    this._pageOffset += sizeof(Decimal);
                } else {
                    int[] words = Decimal.GetBits(value);
                    this.Write(words[0]);
                    this.Write(words[1]);
                    this.Write(words[2]);
                    this.Write(words[3]);
                }
            }
            #endregion
        }

        class BinaryReader : IBinaryReader {

            ObjectFile<TToken> _parent;
            private MemoryMappedViewAccessor _pageAccessor;
            private long _pageOffset;
            private byte[] _buffer;

            public BinaryReader(ObjectFile<TToken> parent, long entryPage) {
                this._parent = parent;
                this._pageAccessor = this._parent._mappedFile.CreateViewAccessor(entryPage, this._parent.PageSize, MemoryMappedFileAccess.Read);
                this._pageOffset = 0;
                this._buffer = new byte[sizeof(decimal)];
            }

            private void ReadBytes(byte[] byteArray, int count) {
                for(int offset = 0; count > 0; count--) {
                    if(this._pageOffset == this._parent.PageCapacity) {
                        long nextPage = this._pageAccessor.ReadInt64(this._parent.PageCapacity);
                        this._pageAccessor.Dispose();
                        this._pageAccessor = this._parent._mappedFile.CreateViewAccessor(nextPage, this._parent.PageSize, MemoryMappedFileAccess.Read);
                        this._pageOffset = 0;
                    }
                    byteArray[offset++] = this._pageAccessor.ReadByte(this._pageOffset++);
                }
            }

            ~BinaryReader() {
                this.Dispose();
            }

            public void Dispose() {
                if(this._pageAccessor != null) {
                    this._pageAccessor.Dispose();
                    this._pageAccessor = null;
                    this._parent = null;
                    this._pageOffset = -1;
                    this._buffer = null;
                }
            }

            #region IBinaryReader

            public UInt16 ReadUInt16() {
                UInt16 value;
                if(this._pageOffset <= this._parent.CutOffSizeForUInt16) {
                    value = this._pageAccessor.ReadUInt16(this._pageOffset);
                    this._pageOffset += sizeof(UInt16);
                } else {
                    this.ReadBytes(this._buffer, sizeof(UInt16));
                    value = BitConverter.ToUInt16(this._buffer, 0);
                }
                return value;
            }

            public Int16 ReadInt16() {
                Int16 value;
                if(this._pageOffset <= this._parent.CutOffSizeForInt16) {
                    value = this._pageAccessor.ReadInt16(this._pageOffset);
                    this._pageOffset += sizeof(Int16);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Int16));
                    value = BitConverter.ToInt16(this._buffer, 0);
                }
                return value;
            }

            public UInt32 ReadUInt32() {
                UInt32 value;
                if(this._pageOffset <= this._parent.CutOffSizeForUInt32) {
                    value = this._pageAccessor.ReadUInt32(this._pageOffset);
                    this._pageOffset += sizeof(UInt32);
                } else {
                    this.ReadBytes(this._buffer, sizeof(UInt32));
                    value = BitConverter.ToUInt32(this._buffer, 0);
                }
                return value;
            }

            public Int32 ReadInt32() {
                Int32 value;
                if(this._pageOffset <= this._parent.CutOffSizeForInt32) {
                    value = this._pageAccessor.ReadInt32(this._pageOffset);
                    this._pageOffset += sizeof(Int32);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Int32));
                    value = BitConverter.ToInt32(this._buffer, 0);
                }
                return value;
            }

            public UInt64 ReadUInt64() {
                UInt64 value;
                if(this._pageOffset <= this._parent.CutOffSizeForUInt64) {
                    value = this._pageAccessor.ReadUInt64(this._pageOffset);
                    this._pageOffset += sizeof(UInt64);
                } else {
                    this.ReadBytes(this._buffer, sizeof(UInt64));
                    value = BitConverter.ToUInt64(this._buffer, 0);
                }
                return value;
            }

            public Int64 ReadInt64() {
                Int64 value;
                if(this._pageOffset <= this._parent.CutOffSizeForInt64) {
                    value = this._pageAccessor.ReadInt64(this._pageOffset);
                    this._pageOffset += sizeof(Int64);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Int64));
                    value = BitConverter.ToInt64(this._buffer, 0);
                }
                return value;
            }

            public Single ReadSingle() {
                Single value;
                if(this._pageOffset <= this._parent.CutOffSizeForSingle) {
                    value = this._pageAccessor.ReadSingle(this._pageOffset);
                    this._pageOffset += sizeof(Single);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Single));
                    value = BitConverter.ToSingle(this._buffer, 0);
                }
                return value;
            }

            public Double ReadDouble() {
                Double value;
                if(this._pageOffset <= this._parent.CutOffSizeForDouble) {
                    value = this._pageAccessor.ReadDouble(this._pageOffset);
                    this._pageOffset += sizeof(Double);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Double));
                    value = BitConverter.ToDouble(this._buffer, 0);
                }
                return value;
            }

            public Char ReadChar() {
                Char value;
                if(this._pageOffset <= this._parent.CutOffSizeForChar) {
                    value = this._pageAccessor.ReadChar(this._pageOffset);
                    this._pageOffset += sizeof(Char);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Char));
                    value = BitConverter.ToChar(this._buffer, 0);
                }
                return value;
            }

            public Boolean ReadBoolean() {
                Boolean value;
                if(this._pageOffset <= this._parent.CutOffSizeForBoolean) {
                    value = this._pageAccessor.ReadBoolean(this._pageOffset);
                    this._pageOffset += sizeof(Boolean);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Boolean));
                    value = BitConverter.ToBoolean(this._buffer, 0);
                }
                return value;
            }

            public Byte ReadByte() {
                Byte value;
                if(this._pageOffset <= this._parent.CutOffSizeForByte) {
                    value = this._pageAccessor.ReadByte(this._pageOffset);
                    this._pageOffset += sizeof(Byte);
                } else {
                    this.ReadBytes(this._buffer, sizeof(Byte));
                    value = this._buffer[0];
                }
                return value;
            }

            public Decimal ReadDecimal() {
                Decimal value;
                if(this._pageOffset <= this._parent.CutOffSizeForDecimal) {
                    value = this._pageAccessor.ReadDecimal(this._pageOffset);
                    this._pageOffset += sizeof(Decimal);
                } else {
                    value = new Decimal(new int[]{
                        this.ReadInt32(),
                        this.ReadInt32(),
                        this.ReadInt32(),
                        this.ReadInt32()
                    });
                }
                return value;
            }

            #endregion
        }

        private MemoryMappedFile _mappedFile;
        private ConcurrentQueue<long> _freePages;
        private ConcurrentDictionary<TToken, long> _entryPages;

        internal readonly long CutOffSizeForInt16;
        internal readonly long CutOffSizeForUInt16;
        internal readonly long CutOffSizeForInt32;
        internal readonly long CutOffSizeForUInt32;
        internal readonly long CutOffSizeForInt64;
        internal readonly long CutOffSizeForUInt64;
        internal readonly long CutOffSizeForSingle;
        internal readonly long CutOffSizeForDouble;
        internal readonly long CutOffSizeForDecimal;
        internal readonly long CutOffSizeForBoolean;
        internal readonly long CutOffSizeForChar;
        internal readonly long CutOffSizeForByte;

        public DirectoryInfo WorkingDirectory { get; private set; }
        public Guid ID { get; private set; }
        public long PageSize { get; private set; }
        public long PageCapacity { get; private set; }
        public long PageCount { get; private set; }
        public long MaxPageCount { get; private set; }

        public ObjectFile(DirectoryInfo workingDirectory, Guid id, int pageCapacity, long maxPageCount) {
            this.WorkingDirectory = workingDirectory;
            this.ID = id;
            if(pageCapacity < sizeof(long)) {
                throw new Exception();
            }
            this.PageCapacity = pageCapacity;
            this.PageSize = this.PageCapacity + sizeof(long);
            this.MaxPageCount = maxPageCount;
            this.PageCount = 0;

            this._mappedFile = MemoryMappedFile.CreateFromFile(this.WorkingDirectory.FullName + @"\" + this.ID.ToString() + ".dat", FileMode.Create, this.ID.ToString(), this.MaxPageCount * this.PageSize, MemoryMappedFileAccess.ReadWrite);
            this._freePages = new ConcurrentQueue<long>();
            this._entryPages = new ConcurrentDictionary<TToken, long>();

            this.CutOffSizeForInt16 = this.PageCapacity - sizeof(Int16);
            this.CutOffSizeForUInt16 = this.PageCapacity - sizeof(UInt16);
            this.CutOffSizeForInt32 = this.PageCapacity - sizeof(Int32);
            this.CutOffSizeForUInt32 = this.PageCapacity - sizeof(UInt32);
            this.CutOffSizeForInt64 = this.PageCapacity - sizeof(Int64);
            this.CutOffSizeForUInt64 = this.PageCapacity - sizeof(UInt64);
            this.CutOffSizeForSingle = this.PageCapacity - sizeof(Single);
            this.CutOffSizeForDouble = this.PageCapacity - sizeof(Double);
            this.CutOffSizeForDecimal = this.PageCapacity - sizeof(Decimal);
            this.CutOffSizeForBoolean = this.PageCapacity - sizeof(Boolean);
            this.CutOffSizeForChar = this.PageCapacity - sizeof(Char);
            this.CutOffSizeForByte = this.PageCapacity - sizeof(Byte);
        }

        public ICollection<TToken> Tokens {
            get {
                return this._entryPages.Keys;
            }
        }

        public IBinaryReader Read(TToken token) {
            long entryPage;
            if(!this._entryPages.TryGetValue(token, out entryPage)) {
                return null;
            }
            return new BinaryReader(this, entryPage);
        }

        public IBinaryWriter Write(TToken token) {
            IBinaryWriter writer = null;
            long entryPage = this._entryPages.GetOrAdd(token, newToken => {
                writer = new BinaryWriter(this, out entryPage);
                return entryPage;
            });

            if(writer == null) {
                writer = new BinaryWriter(this, entryPage);
            }
            return writer;
        }

        public bool Remove(TToken token) {
            long entryPage;
            if(!this._entryPages.TryRemove(token, out entryPage)) {
                return false;
            }
            this._freePages.Enqueue(entryPage);
            return true;
        }
    }
}
